/*
 *  Copyright 2007-2008 by Matthias Buch-Kromann <mbk.isv@cbs.dk>.
 *  *
 *  This file is part of the Open-source Dependency Toolkit (OSDT),
 *  see http://code.google.com/p/open-source-dependency-toolkit.
 *  
 *  The OSDT is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as 
 *  published by the Free Software Foundation, either version 3 of 
 *  the License, or (at your option) any later version.
 * 
 *  The OSDT is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 * 
 *  You should have received a copy of the GNU Lesser General Public 
 *  License along with the OSDT in the files COPYING and COPYING.LESSER.
 *  If not, see http://www.gnu.org/licenses.
 */

package org.osdtsystem.functions;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.osdtsystem.exceptions.FunctionRuntimeException;
import org.osdtsystem.graphs.GraphAccessor;
import org.osdtsystem.values.Location;
import org.osdtsystem.values.NodeID;

/**
 * An abstract function class containing methods shared by all functions. 
 * Every function that extends this abstract class should call the abstract
 * constructor, specifying the subexpressions that can be validly called
 * from the constructed function. Apart from the functions listed among the
 * subexpressions, the only functions that can be called are the named functions
 * returned by namedFunctions().
 * @param <V> the type of the values returned by the abstract function
 * @author Matthias Buch-Kromann <mbk.isv@cbs.dk>
 */
public abstract class AbstractFunction<V> implements Function<V> {
    // What are the IDs for the permitted subfunctions and named functions?
    private List<Function<?>> subfunctions;
    private List<String> freeVariables;
    private List<String> localBoundVariables = Collections.emptyList();
    private List<String> localFreeVariables = Collections.emptyList();
    private List<String> localNamedFunctions = Collections.emptyList();

    // Does this function contain a subfunction that calls a named function?
    private boolean callsNamedFunction;
    private boolean callsNamedFunctionLocally;
    private boolean isUnary = false;
    private boolean isConstant = false;
    private byte height = 0;

    // Function name
    private String name;
    boolean callingToString = false;

    /**
     * Create a new abstract function containing the listed subexpressions.
     * @param subfunctions 
     */
    AbstractFunction(Function<?>... subfunctions) {
        // Store subfunctions
        this.subfunctions = Arrays.asList(subfunctions);
        compile();
    }

    private final void compile() {
        // Compute whether the function calls a named function locally
        if (localNamedFunctions.size() > 0)
            callsNamedFunctionLocally = callsNamedFunction = true;

        // Compute list of free variables
        Set<String> freeVariablesSet = new HashSet<String>();
        for (Function f : subfunctions()) {
                if (f.height() > height)
                    height = f.height();
                freeVariablesSet.addAll(f.freeVariables());
                if (f.callsNamedFunction())
                    callsNamedFunction = true;
        }
        freeVariablesSet.addAll(localFreeVariables);
        freeVariablesSet.removeAll(localBoundVariables);
        this.freeVariables = Arrays.asList(freeVariablesSet.toArray(new String[0]));
        
        // Determine various properties of the function
        height += 1;
        isUnary = freeVariables.size() == 1;
        isConstant = freeVariables.size() == 0;
    }

    @Override
    public final void addLocalNamedFunction(String name) {
        localNamedFunctions = new ArrayList<String>(localNamedFunctions);
        localNamedFunctions.add(name);
        compile();
    }

    @Override
    public final void addLocalFreeVariable(String name) {
        localFreeVariables = new ArrayList<String>(localFreeVariables);
        localFreeVariables.add(name);
        compile();
    }

    @Override
    public final void addLocalBoundVariable(String name) {
        localBoundVariables = new ArrayList<String>(localBoundVariables);
        localBoundVariables.add(name);
        compile();
    }

    // Final functions that are never overridden
    @Override
    public final List<Function<?>> subfunctions() {
        return subfunctions;
    }
    
    @Override
    public final boolean callsNamedFunctionLocally() {
        return callsNamedFunctionLocally;
    }

    @Override
    public final boolean callsNamedFunction() {
        return callsNamedFunction;
    }

    @Override
    public final List<String> freeVariables() {
        return freeVariables;
    }

    @Override
    public final byte height() {
        return height;
    }

    @Override
    public final boolean isUnary() {
        return isUnary;
    }

    @Override
    public final boolean isConstant() {
        return isConstant;
    }

    @Override
    public final String unaryVariable() {
        return isUnary ? freeVariables.get(0) : null;
    }

    @Override
    public boolean restoreContextState() {
        return true;
    }

    @Override
    public boolean isFunction() {
        return true;
    }

    @Override
    public String name() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public abstract List<V> _functionValue(LocalFunction<?, ?> caller, FunctionContext context, GraphAccessor accessor);

    @Override
    public final List<V> functionValue(LocalFunction<?, ?> caller, FunctionContext context, GraphAccessor accessor) {
        try {
            return _functionValue(caller, context, accessor);
        } catch (FunctionRuntimeException e) {
            throw e;
        } catch (Exception e) {
            if (! callingToString) {
                throw new FunctionRuntimeException("Exception caught during function evaluation.\n" +
                        "    function:\n" + this.toString("      ", "      ", caller, context, accessor) +
                        "    context: " + (context != null ? context.toString() : "null") + "\n" +
                        "    accessor: " + (accessor != null ? accessor.toString() : "null") + "\n" +
                        "    exception: " + e.toString() + "\n" +
                        "    caller:\n" + (caller != null ? caller.toString("      ", "      ", caller, context, accessor) : "null") + "\n", e);
            } else {
                return null;
            }
        }
    }

    @Override
    public List<V> functionValue(Location location, GraphAccessor accessor) {
        FunctionContext context = new FunctionContext();
        context.pushVariable(location instanceof NodeID
                ? FunctionContext.ThisNodeVariableName
                : FunctionContext.ThisEdgeVariableName, location);
        return functionValue(null, context, accessor);
    }

    @Override
    public final String toString() {
        return toString("", "", null, null, null);
    }

    @Override
    public final String toString(String prefix) {
        return toString(prefix, prefix, null, null, null);
    }

    @Override
    public final String toString(String prefix1, String prefix) {
        return toString(prefix1, prefix, null, null, null);
    }

    @Override
    public final String toString(String prefix1, String prefix, 
            LocalFunction<?, ?> caller, FunctionContext context, GraphAccessor accessor) {
        if (callingToString) {
            return "RECURSION!!!";
        } else {
            callingToString = true;
            FunctionStringBuilder builder = new FunctionStringBuilder(this);
            toStringBuilder(builder);
            String s = builder.toString(prefix1, prefix, caller, context, accessor);
            callingToString = false;
            return s;
        }
    }
    
    abstract void toStringBuilder(FunctionStringBuilder builder);

    @Override
    public boolean callingToString() {
        return callingToString;
    }

    @Override
    public void setCallingToString(boolean callingToString) {
        this.callingToString = callingToString;
    }

    public static final String INDENT = "  ";    
}
